package crmdna.common;

import java.io.PrintWriter;
import java.io.StringWriter;
import java.io.UnsupportedEncodingException;
import java.net.MalformedURLException;
import java.net.URL;
import java.net.URLEncoder;
import java.text.DecimalFormat;
import java.util.ArrayList;
import java.util.Date;
import java.util.Enumeration;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.TreeMap;
import java.util.TreeSet;
import java.util.logging.Logger;
import java.util.regex.Pattern;

import javax.servlet.http.HttpServletRequest;

import com.google.appengine.api.users.User;

import crmdna.common.APIResponse.Status;
import crmdna.common.config.ConfigCRMDNA;
import crmdna.email.EmailProp;
import crmdna.email.GAEEmail;
import crmdna.refdata.CountryProp;
import crmdna.refdata.RefData;

public class Utils {

	public enum Currency {
		SGD, USD, INR, MYR, AUD, GBP
	}

	public enum PaypalErrorType {
		PAYPAL_SET_EXPRESS_CHECKOUT_FAILURE, PAYPAL_GET_EXPRESS_CHECKOUT_FAILURE, PAYPAL_DO_EXPRESS_CHECKOUT_FAILURE
	}

	public enum PhoneNoType {
		LANDLINE, MOBILE
	}
	
	public static Set<String> getQSTags(String... fields) {
		// creates all combinations of 3 consecutive character as a set
		// eg: if invoked with getQSTags("sathya", "thilakan") the set
		// will be populated as: sat, ath, thy, hya, thi, hil, ila, aka, kan

		Set<String> set = new TreeSet<>();

		for (int i = 0; i < fields.length; i++) {
			if (fields[i] == null)
				continue;

			fields[i] = fields[i].toLowerCase();
			if (fields[i].length() < 3)
				continue;

			for (int beginIndex = 0; beginIndex < fields[i].length() - 2; beginIndex++) {
				set.add(fields[i].substring(beginIndex, beginIndex + 3));
			}
		}

		return set;
	}

	public static boolean closeEnough(String s1, String s2) {

		if ((s1 == null) || (s2 == null))
			return false;

		s1 = s1.toLowerCase().replaceAll(" ", "");
		s2 = s2.toLowerCase().replaceAll(" ", "");

		// if less than or equal to 3 char, entire string should match
		if (s1.length() <= 3)
			return s1.equals(s2);

		// if first 3 chars match return true
		if (s1.substring(0, 3).equals(s2.substring(0, 3)))
			return true;

		Set<String> s1QSTags = getQSTags(s1);
		Set<String> s2QSTags = getQSTags(s2);

		s1QSTags.retainAll(s2QSTags);

		return s1QSTags.size() > Math.min(s1.length(), s1.length()) / 3;
	}

	public static boolean isValidEmailAddress(String email) {
		if (null == email)
			return false;

		boolean stricterFilter = true;
		String stricterFilterString = "[A-Z0-9a-z._%+-]+@[A-Za-z0-9.-]+\\.[A-Za-z]{2,4}";
		String laxString = ".+@.+\\.[A-Za-z]{2}[A-Za-z]*";
		String emailRegex = stricterFilter ? stricterFilterString : laxString;
		java.util.regex.Pattern p = java.util.regex.Pattern.compile(emailRegex);
		java.util.regex.Matcher m = p.matcher(email);
		return m.matches();
	}

	public static boolean isPresentInListCaseInsensitive(List<String> list,
			String s) {

		if ((null == list) || (null == s))
			return false;

		for (String element : list) {
			if (element.equalsIgnoreCase(s))
				return true;
		}

		return false;
	}

	public static String getLoginEmail(User user) {
		if (null == user)
			return null;

		return user.getEmail();
	}

	public static void throwIncorrectSpecException(String message) {
		throw new APIException(Status.ERROR_RESOURCE_INCORRECT, message);
	}

	public static void throwNotFoundException(String message) {
		throw new APIException(Status.ERROR_RESOURCE_NOT_FOUND, message);
	}

	public static void ensureValidPhoneNumber(String phoneNumber) {
		// TODO: remove code duplication in isValidPhoneNumber function
		final String phoneNumberRegex = "\\+(9[976]\\d|8[987530]\\d|6[987]\\d|5[90]\\d|42\\d|3[875]\\d|"
				+ "2[98654321]\\d|9[8543210]|8[6421]|6[6543210]|5[87654321]|"
				+ "4[987654310]|3[9643210]|2[70]|7|1)\\d{1,14}$";

		if ((phoneNumber == null) || (phoneNumber.equals("")))
			throw new APIException(Status.ERROR_RESOURCE_INCORRECT,
					"Phone number cannot be null or empty");

		if (phoneNumber.charAt(0) != '+')
			throw new APIException(Status.ERROR_RESOURCE_INCORRECT,
					"First character of phone number should be +");

		if (!phoneNumber.matches(phoneNumberRegex))
			throw new APIException(Status.ERROR_RESOURCE_INCORRECT,
					"Phone number is invalid");
	}

	public static boolean isValidPhoneNumber(String phoneNumber) {

		// Make this function also take in country as input and do a stricter
		// validation

		final String phoneNumberRegex = "\\+(9[976]\\d|8[987530]\\d|6[987]\\d|5[90]\\d|42\\d|3[875]\\d|"
				+ "2[98654321]\\d|9[8543210]|8[6421]|6[6543210]|5[87654321]|"
				+ "4[987654310]|3[9643210]|2[70]|7|1)\\d{1,14}$";

		if ((phoneNumber == null) || phoneNumber.equals(""))
			return false;

		if (phoneNumber.charAt(0) != '+')
			return false;

		if (!phoneNumber.matches(phoneNumberRegex))
			return false;

		return true;
	}

	public static String getPhoneNoErrMsgIfAnyElseNull(String phoneNumber,
			String country, PhoneNoType phoneNoType) {

		// Make this function also take in country as input and do a stricter
		// validation

		CountryProp countryProp = RefData.getCountryProp(country);
		if (countryProp == null)
			return "["
					+ country
					+ "] is not a valid country or not yet added to IshaCRM. To add a country to IshaCRM please email ["
					+ "sathya.t@ishafoundation.org]";

		if (phoneNoType == PhoneNoType.LANDLINE) {
			if (!phoneNumber.matches(countryProp.landlineRegex))
				return countryProp.messageIfError;
		} else if (phoneNoType == PhoneNoType.MOBILE) {
			if (!phoneNumber.matches(countryProp.mobileRegex))
				return countryProp.messageIfError;
		} else {
			// should never come here
			throw new APIException().status(Status.ERROR_INTERNAL).message(
					"Internal error when validating phone no");
		}

		return null;
	}

	public static String getPhoneNoErrMsgIfAnyElseNull(String phoneNo,
			String country) {

		// Make this function also take in country as input and do a stricter
		// validation

		CountryProp countryProp = RefData.getCountryProp(country);
		if (countryProp == null)
			return "["
					+ country
					+ "] is not a valid country or not yet added to IshaCRM. To add a country to IshaCRM please email ["
					+ "sathya.t@ishafoundation.org]";

		if (!phoneNo.matches(countryProp.landlineRegex)
				&& !phoneNo.matches(countryProp.mobileRegex)) {
			return countryProp.messageIfError;
		}

		return null;
	}

	public static void ensureValidEmail(String email) {
		if ((email == null) || (email.equals("")))
			throw new APIException(Status.ERROR_RESOURCE_INCORRECT,
					"Email cannot be null or empty");

		boolean valid = Utils.isValidEmailAddress(email);

		if (valid == false)
			throw new APIException(Status.ERROR_RESOURCE_INCORRECT, "Email ["
					+ email + "] is invalid");
	}

	public static void ensureValidUrl(String url) {

		try {
			new URL(url);
		} catch (MalformedURLException e) {
			Utils.throwIncorrectSpecException("URL [" + url + "] is invalid");
		}
	}

	public static long safeParseAsLong(String s) {
		try {
			long l = Long.parseLong(s);
			return l;
		} catch (NumberFormatException e) {
			throw new APIException(Status.ERROR_RESOURCE_INCORRECT,
					"Cannot parse [" + s + "] as long");
		}
	}

	public static boolean canParseAsLong(String s) {
		try {
			safeParseAsLong(s);
			return true;
		} catch (Exception ex) {
			return false;
		}
	}

	public static int safeParseAsInt(String s) {
		try {
			int i = Integer.parseInt(s);
			return i;
		} catch (NumberFormatException e) {
			throw new APIException(Status.ERROR_RESOURCE_INCORRECT,
					"Cannot parse [" + s + "] as integer");
		}
	}

	public static double safeParseAsDouble(String s) {
		try {
			double d = Double.parseDouble(s);
			return d;
		} catch (NumberFormatException e) {
			throw new APIException(Status.ERROR_RESOURCE_INCORRECT,
					"Cannot parse [" + s + "] as double");
		}
	}

	
	@SafeVarargs
	public static <T> List<T> getList(T... elements) {
		List<T> list = new ArrayList<>();

		for (T element : elements) {
			list.add(element);
		}

		return list;
	}

	public static String getUrl(String baseUrl, Map<String, Object> queryParams) {
		Utils.ensureValidUrl(baseUrl);

		if (queryParams == null)
			return baseUrl;

		// sort query params by key
		Map<String, Object> treeMap = new TreeMap<>();
		treeMap.putAll(queryParams);

		queryParams = treeMap;

		StringBuilder builder = new StringBuilder(baseUrl);
		if (!baseUrl.contains("?"))
			builder.append("?");
		else
			builder.append("&");

		for (String key : queryParams.keySet()) {
			try {
				final String ENCODING_SCHEME = "UTF-8";
				String encodedKey = URLEncoder.encode(key, ENCODING_SCHEME);
				String encodedValue = URLEncoder.encode(queryParams.get(key)
						.toString(), ENCODING_SCHEME);

				if (encodedValue.length() > 250)
					encodedValue = encodedValue.substring(0, 250);

				builder.append(encodedKey + "=" + encodedValue + "&");
			} catch (UnsupportedEncodingException e) {
				e.printStackTrace();

				throw new RuntimeException(
						"Unsupported Encoding Exception. Message: "
								+ e.getMessage());
			}
		}

		String url = builder.toString();

		// space should be encoded as %20, java encoder encodes it as a +
		url = url.replaceAll(Pattern.quote("+"), "%20");

		// should be valid - but just in case
		Utils.ensureValidUrl(url);

		return url;
	}

	public static void ensureNonNegative(double n) {
		if (n < 0)
			throwIncorrectSpecException("Specified number [" + n
					+ "] is negative");
	}

	public static int getNumDays(Date former, Date later) {
		final int MILLI_SECONDS_IN_A_DAY = 3600 * 24 * 1000;

		int numDays = (int) (former.getTime() - later.getTime())
				/ MILLI_SECONDS_IN_A_DAY;

		return numDays;
	}

	public static String getFullName(String firstName, String lastName) {
		if ((firstName == null) && (lastName == null))
			return null;

		if (firstName == null)
			return lastName;

		if (lastName == null)
			return firstName;

		return firstName + " " + lastName;
	}

	public static String asCurrencyString(double d) {
		DecimalFormat df = new DecimalFormat("#.00");
		return df.format(d);
	}

	public static String urlEncode(String url) {
		try {
			url = URLEncoder.encode(url, "UTF-8");
		} catch (UnsupportedEncodingException e) {
			throw new APIException(Status.ERROR_OPERATION_NOT_ALLOWED,
					"Exception thrown by URLEncoder.encode for url [" + url
							+ "]");
		}

		url.replaceAll(Pattern.quote("+"), "%20");
		return url;
	}

	public static void ensureNotNullOrEmpty(String value, String errMessage) {
		if ((value == null) || value.equals(""))
			Utils.throwIncorrectSpecException(errMessage);
	}

	public static void ensureNonZero(long value, String errMessage) {
		if (value == 0)
			Utils.throwIncorrectSpecException(errMessage);
	}

	public static String csvEncode(String s) {
		if (s == null)
			return "";

		if (s.contains(","))
			return "\"" + s + "\"";

		return s;
	}

	public static String stackTraceToString_old(Throwable e) {
		StringWriter sw = new StringWriter();
		e.printStackTrace(new PrintWriter(sw));

		return sw.toString();
	}

	public static String stackTraceToString(Throwable e) {

		if (e == null)
			return null;

		StringBuilder builder = new StringBuilder();

		builder.append(e.getMessage() + "<br>");

		StackTraceElement[] stElements = e.getStackTrace();

		for (int i = 0; i < stElements.length; i++) {
			builder.append(stElements[i].toString());
			builder.append("<br>");
		}

		return builder.toString();
	}

	// TODO: move this to incidents package later
	public static void sendAlertEmailToDevTeam(String client, Exception ex,
			HttpServletRequest req, String login) {

		try {
			if ((ex == null) || (req == null))
				return;

			// to address
			EmailProp emailProp = new EmailProp();
			emailProp.toEmailAddresses = ConfigCRMDNA.get().toProp().devTeamEmails;

			if (emailProp.toEmailAddresses.size() == 0)
				emailProp.toEmailAddresses.add(crmdna.user.User.SUPER_USER);

			// subject
			emailProp.subject = "Unhandled exception for [" + client + "]: "
					+ ex.getMessage();

			// body
			StringBuilder builder = new StringBuilder();
			builder.append("<br><i>Logged in user:</i> " + login + "<br><br>");
			builder.append("<i>Timestamp:</i> " + new Date() + "<br><br>");

			builder.append("<i>Request:</i> " + req.getRequestURI()
					+ "<br><br>");

			@SuppressWarnings("unchecked")
			Enumeration<String> parameterNames = req.getParameterNames();

			if (parameterNames.hasMoreElements()) {
				builder.append("<i>Query parameters: </i><br>");
				while (parameterNames.hasMoreElements()) {
					String key = (String) parameterNames.nextElement();
					String value = req.getParameter(key);
					builder.append(key + ": " + value + "<br>");
				}
			}
			builder.append("<br>");

			@SuppressWarnings("unchecked")
			Enumeration<String> headerNames = req.getParameterNames();
			if (headerNames.hasMoreElements()) {
				builder.append("<i>Headers: </i><br>");
				while (headerNames.hasMoreElements()) {
					String key = (String) parameterNames.nextElement();
					String value = req.getParameter(key);
					builder.append(key + ": " + value + "<br>");
				}
			}

			builder.append("<br>");
			builder.append("<i>Exception message:</i> " + ex.getMessage());
			builder.append("<br><br>");
			builder.append("<i>Stack trace:</i><br>");
			builder.append(stackTraceToString(ex));

			emailProp.bodyHtml = builder.toString();

			GAEEmail.send(emailProp);
		} catch (Exception exception) {
			// This is usually called in response to a unhandled exception.
			// just log the exception and swallow it
			Logger logger = Logger.getLogger(Utils.class.getName());
			logger.severe(stackTraceToString(exception));
		}
	}

	// TODO: move this to incidents package later
	public static void sendAlertEmailToDevTeam(Exception ex,
			RequestInfo requestInfo) {

		try {
			if (ex == null)
				return;

			// requestInfo can be null

			// to address
			EmailProp emailProp = new EmailProp();
			emailProp.toEmailAddresses = ConfigCRMDNA.get().toProp().devTeamEmails;

			if (emailProp.toEmailAddresses.size() == 0)
				emailProp.toEmailAddresses.add(crmdna.user.User.SUPER_USER);

			String client = "Not Available";
			if ((requestInfo != null) && (requestInfo.getClient() != null))
				client = requestInfo.getClient();

			// subject
			emailProp.subject = "Unhandled exception for [" + client + "]: "
					+ ex.getMessage();

			// body
			String login = "Not Available";
			if ((requestInfo != null) && (requestInfo.getLogin() != null))
				login = requestInfo.getLogin();
			StringBuilder builder = new StringBuilder();
			builder.append("<br><i>Logged in user:</i> " + login + "<br><br>");
			builder.append("<i>Timestamp:</i> " + new Date() + "<br><br>");

			String requestURI = "Not Available";
			if ((requestInfo != null) && (requestInfo.getReq() != null)) {
				requestURI = requestInfo.getReq().getRequestURI();

				builder.append("<i>Request:</i> " + requestURI + "<br><br>");

				@SuppressWarnings("unchecked")
				Enumeration<String> parameterNames = requestInfo.getReq()
						.getParameterNames();

				if (parameterNames.hasMoreElements()) {
					builder.append("<i>Query parameters: </i><br>");
					while (parameterNames.hasMoreElements()) {
						String key = (String) parameterNames.nextElement();
						String value = requestInfo.getReq().getParameter(key);
						builder.append(key + ": " + value + "<br>");
					}
				}

				builder.append("<br>");

				@SuppressWarnings("unchecked")
				Enumeration<String> headerNames = requestInfo.getReq()
						.getParameterNames();
				if (headerNames.hasMoreElements()) {
					builder.append("<i>Headers: </i><br>");
					while (headerNames.hasMoreElements()) {
						String key = (String) parameterNames.nextElement();
						String value = requestInfo.getReq().getParameter(key);
						builder.append(key + ": " + value + "<br>");
					}
				}
			}

			builder.append("<br>");
			builder.append("<i>Exception message:</i> " + ex.getMessage());
			builder.append("<br><br>");
			builder.append("<i>Stack trace:</i><br>");
			builder.append(stackTraceToString(ex));

			emailProp.bodyHtml = builder.toString();

			GAEEmail.send(emailProp);
		} catch (Exception exception) {
			// This is usually called in response to an unhandled exception.
			// just log the exception and swallow it
			Logger logger = Logger.getLogger(Utils.class.getName());
			logger.severe(stackTraceToString(exception));
		}
	}

	public static String sanitizePhoneNo_do_not_use(String phoneNo) {
		if (phoneNo == null)
			return null;

		// remove - and empty spaces
		phoneNo = phoneNo.replaceAll(Pattern.quote("-"), "");
		phoneNo = phoneNo.replaceAll(Pattern.quote(" "), "");

		// replace leading 00 with +
		phoneNo = phoneNo.replace("(^[0][0])", "+");

		// remove leading zeros
		phoneNo = phoneNo.replaceAll("^0+", "");

		if (phoneNo.equals(""))
			return null;

		// if it does not contain + add it
		if ((phoneNo.length() > 1) && (phoneNo.charAt(0) != '+'))
			phoneNo = "+" + phoneNo;

		return phoneNo;
	}

	public static String removeSpacesAndHyphen(String s) {
		if (s == null)
			return null;

		s = s.replaceAll(Pattern.quote(" "), "");
		s = s.replaceAll(Pattern.quote("-"), "");

		return s;
	}

	public static String sanitizePhoneNo(String phoneNo, String country) {
		if (phoneNo == null)
			return null;

		// remove - and empty spaces
		phoneNo = phoneNo.replaceAll(Pattern.quote("-"), "");
		phoneNo = phoneNo.replaceAll(Pattern.quote(" "), "");

		// remove ( and )
		phoneNo = phoneNo.replaceAll(Pattern.quote("("), "");
		phoneNo = phoneNo.replaceAll(Pattern.quote(")"), "");

		// add isd code if not specified
		// for eg: if phone no is 93232152 and country is singapore make it
		// +6593232152
		if (country != null) {
			CountryProp countryProp = RefData.getCountryProp(country);
			if (countryProp != null) {
				if (countryProp.numDigitsWOCountryCode != null) {
					if (phoneNo.length() == countryProp.numDigitsWOCountryCode) {
						phoneNo = countryProp.isdCode + phoneNo;
					}
				}
			}
		}

		// replace leading 00 with ""
		phoneNo = phoneNo.replaceFirst("^(0*)", "");

		if (phoneNo.equals(""))
			return null;

		// if it does not contain + add it
		if ((phoneNo.length() > 1) && (phoneNo.charAt(0) != '+'))
			phoneNo = "+" + phoneNo;

		return phoneNo;
	}

	public static String sanitizeEmail(String email) {
		if ((email == null) || email.equals(""))
			return null;

		return email;
	}

	// TODO: check if this method is really required. probably can be removed
	public static <T> String toUserFriendlyString(Iterable<T> iterable) {
		if (iterable == null)
			return null;

		StringBuilder builder = new StringBuilder();

		int lineNo = 0;
		for (T t : iterable) {
			lineNo++;

			builder.append("<br>" + lineNo + ") - " + t);
		}

		return builder.toString();
	}

	public static APIResponse toAPIResponse(Exception ex,
			Boolean showStackTrace, RequestInfo requestInfo) {

		APIResponse apiResponse;
		if (ex instanceof APIException) {
			APIException apiException = (APIException) ex;
			apiResponse = apiException.toAPIResponse();

			if (apiResponse.statusCode == Status.ERROR_INTERNAL)
				sendAlertEmailToDevTeam(apiException, requestInfo);

		} else {
			apiResponse = new APIResponse();
			apiResponse.statusCode = Status.ERROR_UNHANDLED_EXCEPTION;
			apiResponse.userFriendlyMessage = ex.getMessage();
			apiResponse.stackTraceElements = ex.getStackTrace();

			Utils.sendAlertEmailToDevTeam(ex, requestInfo);
		}

		if ((showStackTrace == null) || (showStackTrace == false))
			apiResponse.stackTraceElements = null;

		return apiResponse;
	}

	public static boolean isDifferentCaseInsensitive(String s1, String s2) {
		if ((s1 == null) || (s2 == null))
			return true;

		return !s1.toLowerCase().equals(s2.toLowerCase());
	}
}
